1 /**
2    Creates test cases from compile-time information.
3  */
4 module unit_threaded.factory;
5 
6 import unit_threaded.from;
7 import unit_threaded.testcase : CompositeTestCase;
8 
9 private CompositeTestCase[string] serialComposites;
10 
11 /**
12  * Creates tests cases from the given modules.
13  * If testsToRun is empty, it means run all tests.
14  */
15 from!"unit_threaded.testcase".TestCase[] createTestCases(
16         in from!"unit_threaded.reflection".TestData[] testData, in string[] testsToRun = []) {
17     import unit_threaded.testcase : TestCase;
18     import std.algorithm : sort;
19     import std.array : array;
20 
21     serialComposites = null;
22     bool[TestCase] tests;
23     foreach (const data; testData) {
24         if (!isWantedTest(data, testsToRun))
25             continue;
26         auto test = createTestCase(data);
27         if (test !is null)
28             tests[test] = true; //can be null if abtract base class
29     }
30 
31     return tests.keys.sort!((a, b) => a.getPath < b.getPath).array;
32 }
33 
34 package from!"unit_threaded.testcase".TestCase createTestCase(
35         in from!"unit_threaded.reflection".TestData testData) {
36     import unit_threaded.testcase : TestCase;
37     import std.algorithm : splitter, reduce;
38     import std.array : array;
39 
40     TestCase createImpl() {
41         import unit_threaded.testcase : BuiltinTestCase, FunctionTestCase,
42             ShouldFailTestCase, FlakyTestCase;
43         import std.conv : text;
44 
45         TestCase testCase;
46 
47         if (testData.isTestClass)
48             testCase = cast(TestCase) Object.factory(testData.name);
49         else
50             testCase = testData.builtin ? new BuiltinTestCase(testData)
51                 : new FunctionTestCase(testData);
52 
53         version (unitThreadedLight) {
54         } else
55             assert(testCase !is null, text("Error creating test case with ",
56                     testData.isTestClass ? "test class data: " : "data: ", testData));
57 
58         if (testData.shouldFail) {
59             testCase = new ShouldFailTestCase(testCase, testData.exceptionTypeInfo);
60         } else if (testData.flakyRetries > 0)
61             testCase = new FlakyTestCase(testCase, testData.flakyRetries);
62 
63         return testCase;
64     }
65 
66     auto testCase = createImpl();
67 
68     if (testData.singleThreaded) {
69         // @Serial tests in the same module run sequentially.
70         // A CompositeTestCase is created for each module with at least
71         // one @Serial test and subsequent @Serial tests
72         // appended to it
73         //const moduleName = testData.name.dup.splitter(".")
74         const moduleName = testData.name.splitter(".").array[0 .. $ - 1].reduce!((a,
75                 b) => a ~ "." ~ b);
76 
77         // create one if not already there
78         if (moduleName !in serialComposites) {
79             serialComposites[moduleName] = new CompositeTestCase;
80         }
81 
82         // add the current test to the composite
83         serialComposites[moduleName] ~= testCase;
84         return serialComposites[moduleName];
85     }
86 
87     assert(testCase !is null || testData.testFunction is null,
88             "Could not create TestCase object for test " ~ testData.name);
89 
90     return testCase;
91 }
92 
93 private bool isWantedTest(in from!"unit_threaded.reflection".TestData testData,
94         in string[] testsToRun) {
95 
96     import std.algorithm : filter, all, startsWith, canFind;
97     import std.array : array;
98 
99     bool isTag(in string t) {
100         return t.startsWith("@") || t.startsWith("~@");
101     }
102 
103     auto normalToRun = testsToRun.filter!(a => !isTag(a)).array;
104     auto tagsToRun = testsToRun.filter!isTag;
105 
106     bool matchesTags(in string tag) { //runs all tests with the specified tags
107         assert(isTag(tag));
108         return tag[0] == '@' && testData.tags.canFind(tag[1 .. $])
109             || (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2 .. $]));
110     }
111 
112     return isWantedNonTagTest(testData, normalToRun) && (tagsToRun.empty
113             || tagsToRun.all!(t => matchesTags(t)));
114 }
115 
116 private bool isWantedNonTagTest(
117         in from!"unit_threaded.reflection".TestData testData, in string[] testsToRun) {
118 
119     import std.algorithm : any, startsWith, canFind;
120 
121     if (!testsToRun.length)
122         return !testData.hidden; //all tests except the hidden ones
123 
124     bool matchesExactly(in string t) {
125         return t == testData.name;
126     }
127 
128     bool matchesPackage(in string t) { //runs all tests in package if it matches
129         with (testData)
130             return !hidden && name.length > t.length && name.startsWith(t)
131                 && name[t.length .. $].canFind(".");
132     }
133 
134     return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a));
135 }
136 
137 unittest {
138     import unit_threaded.reflection : TestData;
139 
140     //existing, wanted
141     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests"]));
142     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests."]));
143     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server.testSubscribe"]));
144     assert(!isWantedTest(TestData("tests.server.testSubscribe"),
145             ["tests.server.testSubscribeWithMessage"]));
146     assert(!isWantedTest(TestData("tests.stream.testMqttInTwoPackets"), ["tests.server"]));
147     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server"]));
148     assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests"]));
149     assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.testEqual"]));
150     assert(isWantedTest(TestData("pass_tests.testEqual"), []));
151     assert(!isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.foo"]));
152     assert(!isWantedTest(TestData("example.tests.pass.normal.unittest"),
153             ["example.tests.pass.io.TestFoo"]));
154     assert(isWantedTest(TestData("example.tests.pass.normal.unittest"), []));
155     assert(!isWantedTest(TestData("tests.pass.attributes.testHidden", null, true  /*hidden*/ ),
156             ["tests.pass"]));
157     assert(!isWantedTest(TestData("", null, false  /*hidden*/ , false  /*shouldFail*/ ,
158             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ ), ["@foo"]));
159     assert(isWantedTest(TestData("", null, false  /*hidden*/ , false  /*shouldFail*/ ,
160             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ , ["foo"]), ["@foo"]));
161 
162     assert(!isWantedTest(TestData("", null, false  /*hidden*/ , false  /*shouldFail*/ ,
163             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ , ["foo"]), ["~@foo"]));
164 
165     assert(isWantedTest(TestData("", null, false  /*hidden*/ , false  /*shouldFail*/ ,
166             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ ), ["~@foo"]));
167 
168     assert(isWantedTest(TestData("", null, false  /*hidden*/ , false  /*shouldFail*/ ,
169             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ , ["bar"]), ["~@foo"]));
170 
171     // if hidden, don't run by default
172     assert(!isWantedTest(TestData("", null, true  /*hidden*/ , false  /*shouldFail*/ ,
173             false  /*singleThreaded*/ , false  /*builtin*/ , "" /*suffix*/ , ["bar"]), ["~@foo"]));
174 
175 }